<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="zh-CN">
<head>
  <title></title>
  <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css" />
  
<style type="text/css">
@page { margin-bottom: 5.000000pt; margin-top: 5.000000pt; }
</style>
</head>

<body>
  <h2><span style="border-bottom:1px solid">Chapter_3</span></h2>

  <p>条注释构成（星号和斜杠以及其中的内容，本章后面还会详细讲述），所以并不能对它做太多的事情。事实上，除非为其定义了某些方法，否则根本不能指示它做任何事情。</p>

  <p>2.4.1 字段和方法</p>

  <p>定义一个类时（我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象），可在自己的类里设置两种类型的元素：数据成员（有时也叫“字段”）以及成员函数（通常叫“方法”）。其中，数据成员是一种对象（通过它的句柄与其通信），可以为任何类型。它也可以是主类型（并不是句柄）之一。如果是指向对象的一个句柄，则必须初始化那个句柄，用一种名为“构建器”（第4章会对此详述）的特殊函数将其与一个实际对象连接起来（就象早先看到的那样，使用new关键字）。但若是一种主类型，则可在类定义位置直接初始化（正如后面会看到的那样，句柄亦可在定义位置初始化）。</p>

  <p>每个对象都为自己的数据成员保有存储空间；数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例：</p>

  <p>class DataOnly {</p>

  <p>int i;</p>

  <p>float f;</p>

  <p>boolean b;</p>

  <p>}</p>

  <p>这个类并没有做任何实质性的事情，但我们可创建一个对象：</p>

  <p>DataOnly d = new DataOnly();</p>

  <p>可将值赋给数据成员，但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的，首先要写上对象句柄的名字，再跟随一个点号（句点），再跟随对象内部成员的名字。即“对象句柄.成员”。例如：</p>

  <p>d.i = 47;</p>

  <p>d.f = 1.1f;</p>

  <p>d.b = false;</p>

  <p>一个对象也可能包含了另一个对象，而另一个对象里则包含了我们想修改的数据。对于这个问题，只需保持“连接句点”即可。例如：</p>

  <p>myPlane.leftTank.capacity = 100;</p>

  <p>除容纳数据之外，DataOnly类再也不能做更多的事情，因为它没有成员函数（方法）。为正确理解工作原理，首先必须知道“自变量”和“返回值”的概念。我们马上就会详加解释。</p>

  <p>1. 主成员的默认值</p>

  <p>若某个主数据类型属于一个类成员，那么即使不明确（显式）进行初始化，也可以保证它们获得一个默认值。</p>

  <p>主类型 默认值</p>

  <p>Boolean false</p>

  <p>Char '\u0000'(null)</p>

  <p>byte (byte)0</p>

  <p>short (short)0</p>

  <p>int 0</p>

  <p>long 0L</p>

  <p>float 0.0f</p>

  <p>double 0.0d</p>

  <p>一旦将变量作为类成员使用，就要特别注意由Java分配的默认值。这样做可保证主类型的成员变量肯定得到了初始化（C++不具备这一功能），可有效遏止多种相关的编程错误。</p>

  <p>然而，这种保证却并不适用于“局部”变量――那些变量并非一个类的字段。所以，假若在一个函数定义中写入下述代码：</p>

  <p>int x;</p>

  <p>那么x会得到一些随机值（这与C和C++是一样的），不会自动初始化成零。我们责任是在正式使用x前分配一个适当的值。如果忘记，就会得到一条编译期错误，告诉我们变量可能尚未初始化。这种处理正是Java优于C++的表现之一。许多C++编译器会对变量未初始化发出警告，但在Java里却是错误。</p>

  <p>2.5 方法、自变量和返回值</p>

  <p>迄今为止，我们一直用“函数”（Function）这个词指代一个已命名的子例程。但在Java里，更常用的一个词却是“方法”（Method），代表“完成某事的途径”。尽管它们表达的实际是同一个意思，但从现在开始，本书将一直使用“方法”，而不是“函数”。</p>

  <p>Java的“方法”决定了一个对象能够接收的消息。通过本节的学习，大家会知道方法的定义有多么简单！</p>

  <p>方法的基本组成部分包括名字、自变量、返回类型以及主体。下面便是它最基本的形式：</p>

  <p>返回类型 方法名( /* 自变量列表*/ ) {/* 方法主体 */}</p>

  <p>返回类型是指调用方法之后返回的数值类型。显然，方法名的作用是对具体的方法进行标识和引用。自变量列表列出了想传递给方法的信息类型和名称。</p>

  <p>Java的方法只能作为类的一部分创建。只能针对某个对象调用一个方法（注释③），而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误的方法，就会在编译期得到一条出错消息。为一个对象调用方法时，需要先列出对象的名字，在后面跟上一个句点，再跟上方法名以及它的参数列表。亦即“对象名.方法名(自变量1，自变量2，自变量3...)。举个例子来说，假设我们有一个方法名叫f()，它没有自变量，返回的是类型为int的一个值。那么，假设有一个名为a的对象，可为其调用方法f()，则代码如下：</p>

  <p>int x = a.f();</p>

  <p>返回值的类型必须兼容x的类型。</p>

  <p>象这样调用一个方法的行动通常叫作“向对象发送一条消息”。在上面的例子中，消息是f()，而对象是a。面向对象的程序设计通常简单地归纳为“向对象发送消息”。</p>

  <p>③：正如马上就要学到的那样，“静态”方法可针对类调用，毋需一个对象。</p>

  <p>2.5.1 自变量列表</p>

  <p>自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样，这些信息――如同Java内其他任何东西――采用的都是对象的形式。因此，我们必须在自变量列表里指定要传递的对象类型，以及每个对象的名字。正如在Java其他地方处理对象时一样，我们实际传递的是“句柄”（注释④）。然而，句柄的类型必须正确。倘若希望自变量是一个“字串”，那么传递的必须是一个字串。</p>

  <p>④：对于前面提及的“特殊”数据类型boolean，char，byte，short，int，long，，float以及double来说是一个例外。但在传递对象时，通常都是指传递指向对象的句柄。</p>

  <p>下面让我们考虑将一个字串作为自变量使用的方法。下面列出的是定义代码，必须将它置于一个类定义里，否则无法编译：</p>

  <p>int storage(String s) {</p>

  <p>return s.length() * 2;</p>

  <p>}</p>

  <p>这个方法告诉我们需要多少字节才能容纳一个特定字串里的信息（字串里的每个字符都是16位，或者说2个字节、长整数，以便提供对Unicode字符的支持）。自变量的类型为String，而且叫作s。一旦将s传递给方法，就可将它当作其他对象一样处理（可向其发送消息）。在这里，我们调用的是length()方法，它是String的方法之一。该方法返回的是一个字串里的字符数。</p>

  <p>通过上面的例子，也可以了解return关键字的运用。它主要做两件事情。首先，它意味着“离开方法，我已完工了”。其次，假设方法生成了一个值，则那个值紧接在return语句的后面。在这种情况下，返回值是通过计算表达式“s.length()*2”而产生的。</p>

  <p>可按自己的愿望返回任意类型，但倘若不想返回任何东西，就可指示方法返回void（空）。下面列出一些例子。</p>

  <p>boolean flag() { return true; }</p>

  <p>float naturalLogBase() { return 2.718; }</p>

  <p>void nothing() { return; }</p>

  <p>void nothing2() {}</p>

  <p>若返回类型为void，则return关键字唯一的作用就是退出方法。所以一旦抵达方法末尾，该关键字便不需要了。可在任何地方从一个方法返回。但假设已指定了一种非void的返回类型，那么无论从何地返回，编译器都会确保我们返回的是正确的类型。</p>

  <p>到此为止，大家或许已得到了这样的一个印象：一个程序只是一系列对象的集合，它们的方法将其他对象作为自己的自变量使用，而且将消息发给那些对象。这种说法大体正确，但通过以后的学习，大家还会知道如何在一个方法里作出决策，做一些更细致的基层工作。至于这一章，只需理解消息传送就足够了。</p>

  <p>2.6 构建Java程序</p>

  <p>正式构建自己的第一个Java程序前，还有几个问题需要注意。</p>

  <p>2.6.1 名字的可见性</p>

  <p>在所有程序设计语言里，一个不可避免的问题是对名字或名称的控制。假设您在程序的某个模块里使用了一个名字，而另一名程序员在另一个模块里使用了相同的名字。此时，如何区分两个名字，并防止两个名字互相冲突呢？这个问题在C语言里特别突出。因为程序未提供很好的名字管理方法。C++的类（即Java类的基础）嵌套使用类里的函数，使其不至于同其他类里的嵌套函数名冲突。然而，C++仍然允许使用全局数据以及全局函数，所以仍然难以避免冲突。为解决这个问题，C++用额外的关键字引入了“命名空间”的概念。</p>

  <p>由于采用全新的机制，所以Java能完全避免这些问题。为了给一个库生成明确的名字，采用了与Internet域名类似的名字。事实上，Java的设计者鼓励程序员反转使用自己的Internet域名，因为它们肯定是独一无二的。由于我的域名是BruceEckel.com，所以我的实用工具库就可命名为com.bruceeckel.utility.foibles。反转了域名后，可将点号想象成子目录。</p>

  <p>在Java 1.0和Java 1.1中，域扩展名com，edu，org，net等都约定为大写形式。所以库的样子就变成：COM.bruceeckel.utility.foibles。然而，在Java 1.2的开发过程中，设计者发现这样做会造成一些问题。所以目前的整个软件包都以小写字母为标准。</p>

  <p>Java的这种特殊机制意味着所有文件都自动存在于自己的命名空间里。而且一个文件里的每个类都自动获得一个独一无二的标识符（当然，一个文件里的类名必须是唯一的）。所以不必学习特殊的语言知识来解决这个问题――语言本身已帮我们照顾到这一点。</p>

  <p>2.6.2 使用其他组件</p>

  <p>一旦要在自己的程序里使用一个预先定义好的类，编译器就必须知道如何找到它。当然，这个类可能就在发出调用的那个相同的源码文件里。如果是那种情况，只需简单地使用这个类即可――即使它直到文件的后面仍未得到定义。Java消除了“向前引用”的问题，所以不要关心这些事情。</p>

  <p>但假若那个类位于其他文件里呢？您或许认为编译器应该足够“联盟”，可以自行发现它。但实情并非如此。假设我们想使用一个具有特定名称的类，但那个类的定义位于多个文件里。或者更糟，假设我们准备写一个程序，但在创建它的时候，却向自己的库加入了一个新类，它与现有某个类的名字发生了冲突。</p>

  <p>为解决这个问题，必须消除所有潜在的、纠缠不清的情况。为达到这个目的，要用import关键字准确告诉Java编译器我们希望的类是什么。import的作用是指示编译器导入一个“包”――或者说一个“类库”（在其他语言里，可将“库”想象成一系列函数、数据以及类的集合。但请记住，Java的所有代码都必须写入一个类中）。</p>

  <p>大多数时候，我们直接采用来自标准Java库的组件（部件）即可，它们是与编译器配套提供的。使用这些组件时，没有必要关心冗长的保留域名；举个例子来说，只需象下面这样写一行代码即可：</p>

  <p>import java.util.Vector;</p>

  <p>它的作用是告诉编译器我们想使用Java的Vector类。然而，util包含了数量众多的类，我们有时希望使用其中的几个，同时不想全部明确地声明它们。为达到这个目的，可使用“*”通配符。如下所示：</p>

  <p>import java.util.*;</p>

  <p>需导入一系列类时，采用的通常是这个办法。应尽量避免一个一个地导入类。</p>

  <p>2.6.3 static关键字</p>

  <p>通常，我们创建类时会指出那个类的对象的外观与行为。除非用new创建那个类的一个对象，否则实际上并未得到任何东西。只有执行了new后，才会正式生成数据存储空间，并可使用相应的方法。</p>

  <p>但在两种特殊的情形下，上述方法并不堪用。一种情形是只想用一个存储区域来保存一个特定的数据――无论要创建多少个对象，甚至根本不创建对象。另一种情形是我们需要一个特殊的方法，它没有与这个类的任何对象关联。也就是说，即使没有创建对象，也需要一个能调用的方法。为满足这两方面的要求，可使用static（静态）关键字。一旦将什么东西设为static，数据或方法就不会同那个类的任何对象实例联系到一起。所以尽管从未创建那个类的一个对象，仍能调用一个static方法，或访问一些static数据。而在这之前，对于非static数据和方法，我们必须创建一个对象，并用那个对象访问数据或方法。这是由于非static数据和方法必须知道它们操作的具体对象。当然，在正式使用前，由于static方法不需要创建任何对象，所以它们不可简单地调用其他那些成员，同时不引用一个已命名的对象，从而直接访问非static成员或方法（因为非static成员和方法必须同一个特定的对象关联到一起）。</p>

  <p>有些面向对象的语言使用了“类数据”和“类方法”这两个术语。它们意味着数据和方法只是为作为一个整体的类而存在的，并不是为那个类的任何特定对象。有时，您会在其他一些Java书刊里发现这样的称呼。</p>

  <p>为了将数据成员或方法设为static，只需在定义前置和这个关键字即可。例如，下述代码能生成一个static数据成员，并对其初始化：</p>

  <p>class StaticTest {</p>

  <p>Static int i = 47;</p>

  <p>}</p>

  <p>现在，尽管我们制作了两个StaticTest对象，但它们仍然只占据StaticTest.i的一个存储空间。这两个对象都共享同样的i。请考察下述代码：</p>

  <p>StaticTest st1 = new StaticTest();</p>

  <p>StaticTest st2 = new StaticTest();</p>

  <p>此时，无论st1.i还是st2.i都有同样的值47，因为它们引用的是同样的内存区域。</p>

  <p>有两个办法可引用一个static变量。正如上面展示的那样，可通过一个对象命名它，如st2.i。亦可直接用它的类名引用，而这在非静态成员里是行不通的（最好用这个办法引用static变量，因为它强调了那个变量的“静态”本质）。</p>

  <p>StaticTest.i++;</p>

  <p>其中，++运算符会使变量增值。此时，无论st1.i还是st2.i的值都是48。</p>

  <p>类似的逻辑也适用于静态方法。既可象对其他任何方法那样通过一个对象引用静态方法，亦可用特殊的语法格式“类名.方法()”加以引用。静态方法的定义是类似的：</p>

  <p>class StaticFun {</p>

  <p>static void incr() { StaticTest.i++; }</p>

  <p>}</p>

  <p>从中可看出，StaticFun的方法incr()使静态数据i增值。通过对象，可用典型的方法调用incr()：</p>

  <p>StaticFun sf = new StaticFun();</p>

  <p>sf.incr();</p>

  <p>或者，由于incr()是一种静态方法，所以可通过它的类直接调用：</p>

  <p>StaticFun.incr();</p>

  <p>尽管是“静态”的，但只要应用于一个数据成员，就会明确改变数据的创建方式（一个类一个成员，以及每个对象一个非静态成员）。若应用于一个方法，就没有那么戏剧化了。对方法来说，static一项重要的用途就是帮助我们在不必创建对象的前提下调用那个方法。正如以后会看到的那样，这一点是至关重要的――特别是在定义程序运行入口方法main()的时候。</p>

  <p>和其他任何方法一样，static方法也能创建自己类型的命名对象。所以经常把static方法作为一个“领头羊”使用，用它生成一系列自己类型的“实例”。</p>

  <p>2.7 我们的第一个Java程序</p>

  <p>最后，让我们正式编一个程序（注释⑤）。它能打印出与当前运行的系统有关的资料，并利用了来自Java标准库的System对象的多种方法。注意这里引入了一种额外的注释样式：“//”。它表示到本行结束前的所有内容都是注释：</p>

  <p>// Property.java</p>

  <p>import java.util.*;</p>

  <p>public class Property {</p>

  <p>public static void main(String[] args) {</p>

  <p>System.out.println(new Date());</p>

  <p>Properties p = System.getProperties();</p>

  <p>p.list(System.out);</p>

  <p>System.out.println("--- Memory Usage:");</p>

  <p>Runtime rt = Runtime.getRuntime();</p>

  <p>System.out.println("Total Memory = "</p>

  <p>+ rt.totalMemory()</p>

  <p>+ " Free Memory = "</p>

  <p>+ rt.freeMemory());</p>

  <p>}</p>

  <p>}</p>

  <p>⑤：在某些编程环境里，程序会在屏幕上一切而过，甚至没机会看到结果。可将下面这段代码置于main()的末尾，用它暂停输出：</p>

  <p>try {</p>

  <p>Thread.currentThread().sleep(5 * 1000);</p>

  <p>} catch(InterruptedException e) {}</p>

  <p>}</p>

  <p>它的作用是暂停输出5秒钟。这段代码涉及的一些概念要到本书后面才会讲到。所以目前不必深究，只知道它是让程序暂停的一个技巧便可。</p>

  <p>在每个程序文件的开头，都必须放置一个import语句，导入那个文件的代码里要用到的所有额外的类。注意我们说它们是“额外”的，因为一个特殊的类库会自动导入每个Java文件：java.lang。启动您的Web浏览器，查看由Sun提供的用户文档（如果尚未从http://www.java.sun.com下载，或用其他方式安装了Java文档，请立即下载）。在packages.html文件里，可找到Java配套提供的所有类库名称。请选择其中的java.lang。在“Class Index”下面，可找到属于那个库的全部类的列表。由于java.lang默认进入每个Java代码文件，所以这些类在任何时候都可直接使用。在这个列表里，可发现System和Runtime，我们在Property.java里用到了它们。java.lang里没有列出Date类，所以必须导入另一个类库才能使用它。如果不清楚一个特定的类在哪个类库里，或者想检视所有的类，可在Java用户文档里选择“Class Hierarchy”（类分级结构）。在Web浏览器中，虽然要花不短的时间来建立这个结构，但可清楚找到与Java配套提供的每一个类。随后，可用浏览器的“查找”（Find）功能搜索关键字“Date”。经这样处理后，可发现我们的搜索目标以java.util.Date的形式列出。我们终于知道它位于util库里，所以必须导入java.util.*；否则便不能使用Date。</p>

  <p>观察packages.html文档最开头的部分（我已将其设为自己的默认起始页），请选择java.lang，再选System。这时可看到System类有几个字段。若选择out，就可知道它是一个static PrintStream对象。由于它是“静态”的，所以不需要我们创建任何东西。out对象肯定是3，所以只需直接用它即可。我们能对这个out对象做的事情由它的类型决定：PrintStream。PrintStream在说明文字中以一个超链接的形式列出，这一点做得非常方便。所以假若单击那个链接，就可看到能够为PrintStream调用的所有方法。方法的数量不少，本书后面会详细介绍。就目前来说，我们感兴趣的只有println()。它的意思是“把我给你的东西打印到控制台，并用一个新行结束”。所以在任何Java程序中，一旦要把某些内容打印到控制台，就可条件反射地写上System.out.println("内容")。</p>

  <p>类名与文件是一样的。若象现在这样创建一个独立的程序，文件中的一个类必须与文件同名（如果没这样做，编译器会及时作出反应）。类里必须包含一个名为main()的方法，形式如下：</p>

  <p>public static void main(String[] args) {</p>

  <p>其中，关键字“public”意味着方法可由外部世界调用（第5章会详细解释）。main()的自变量是包含了String对象的一个数组。args不会在本程序中用到，但需要在这个地方列出，因为它们保存了在命令行调用的自变量。</p>

  <p>程序的第一行非常有趣：</p>

  <p>System.out.println(new Date());</p>

  <p>请观察它的自变量：创建Date对象唯一的目的就是将它的值发送给println()。一旦这个语句执行完毕，Date就不再需要。随之而来的“垃圾收集器”会发现这一情况，并在任何可能的时候将其回收。事实上，我们没太大的必要关心“清除”的细节。</p>

  <p>第二行调用了System.getProperties()。若用Web浏览器查看联机用户文档，就可知道getProperties()是System类的一个static方法。由于它是“静态”的，所以不必创建任何对象便可调用该方法。无论是否存在该类的一个对象，static方法随时都可使用。调用getProperties()时，它会将系统属性作为Properties类的一个对象生成（注意Properties是“属性”的意思）。随后的的句柄保存在一个名为p的Properties句柄里。在第三行，大家可看到Properties对象有一个名为list()的方法，它将自己的全部内容都发给一个我们作为自变量传递的PrintStream对象。</p>

  <p>main()的第四和第六行是典型的打印语句。注意为了打印多个String值，用加号（+）分隔它们即可。然而，也要在这里注意一些奇怪的事情。在String对象中使用时，加号并不代表真正的“相加”。处理字串时，我们通常不必考虑“+”的任何特殊含义。但是，Java的String类要受一种名为“运算符过载”的机制的制约。也就是说，只有在随同String对象使用时，加号才会产生与其他任何地方不同的表现。对于字串，它的意思是“连接这两个字串”。</p>

  <p>但事情到此并未结束。请观察下述语句：</p>

  <p>System.out.println("Total Memory = "</p>

  <p>+ rt.totalMemory()</p>

  <p>+ " Free Memory = "</p>

  <p>+ rt.freeMemory());</p>

  <p>其中，totalMemory()和freeMemory()返回的是数值，并非String对象。如果将一个数值“加”到一个字串身上，会发生什么情况呢？同我们一样，编译器也会意识到这个问题，并魔术般地调用一个方法，将那个数值（int，float等等）转换成字串。经这样处理后，它们当然能利用加号“加”到一起。这种“自动类型转换”亦划入“运算符过载”处理的范畴。</p>

  <p>许多Java著作都在热烈地辩论“运算符过载”（C++的一项特性）是否有用。目前就是反对它的一个好例子！然而，这最多只能算编译器（程序）的问题，而且只是对String对象而言。对于自己编写的任何源代码，都不可能使运算符“过载”。</p>

  <p>通过为Runtime类调用getRuntime()方法，main()的第五行创建了一个Runtime对象。返回的则是指向一个Runtime对象的句柄。而且，我们不必关心它是一个静态对象，还是由new命令创建的一个对象。这是由于我们不必为清除工作负责，可以大模大样地使用对象。正如显示的那样，Runtime可告诉我们与内存使用有关的信息。</p>

  <p>2.8 注释和嵌入文档</p>

  <p>Java里有两种类型的注释。第一种是传统的、C语言风格的注释，是从C++继承而来的。这些注释用一个“/*”起头，随后是注释内容，并可跨越多行，最后用一个“*/”结束。注意许多程序员在连续注释内容的每一行都用一个“*”开头，所以经常能看到象下面这样的内容：</p>

  <p>/* 这是</p>

  <p>* 一段注释，</p>

  <p>* 它跨越了多个行</p>

  <p>*/</p>

  <p>但请记住，进行编译时，/*和*/之间的所有东西都会被忽略，所以上述注释与下面这段注释并没有什么不同：</p>

  <p>/* 这是一段注释，</p>

  <p>它跨越了多个行 */</p>

  <p>第二种类型的注释也起源于C++。这种注释叫作“单行注释”，以一个“//”起头，表示这一行的所有内容都是注释。这种类型的注释更常用，因为它书写时更方便。没有必要在键盘上寻找“/”，再寻找“*”（只需按同样的键两次），而且不必在注释结尾时加一个结束标记。下面便是这类注释的一个例子：</p>

  <p>// 这是一条单行注释</p>

  <p>2.8.1 注释文档</p>

  <p>对于Java语言，最体贴的一项设计就是它并没有打算让人们为了写程序而写程序――人们也需要考虑程序的文档化问题。对于程序的文档化，最大的问题莫过于对文档的维护。若文档与代码分离，那么每次改变代码后都要改变文档，这无疑会变成相当麻烦的一件事情。解决的方法看起来似乎很简单：将代码同文档“链接”起来。为达到这个目的，最简单的方法是将所有内容都置于同一个文件。然而，为使一切都整齐划一，还必须使用一种特殊的注释语法，以便标记出特殊的文档；另外还需要一个工具，用于提取这些注释，并按有价值的形式将其展现出来。这些都是Java必须做到的。</p>

  <p>用于提取注释的工具叫作javadoc。它采用了部分来自Java编译器的技术，查找我们置入程序的特殊注释标记。它不仅提取由这些标记指示的信息，也将毗邻注释的类名或方法名提取出来。这样一来，我们就可用最轻的工作量，生成十分专业的程序文档。</p>

  <p>javadoc输出的是一个HTML文件，可用自己的Web浏览器查看。该工具允许我们创建和管理单个源文件，并生动生成有用的文档。由于有了jvadoc，所以我们能够用标准的方法创建文档。而且由于它非常方便，所以我们能轻松获得所有Java库的文档。</p>

  <p>2.8.2 具体语法</p>

  <p>所有javadoc命令都只能出现于“/**”注释中。但和平常一样，注释结束于一个“*/”。主要通过两种方式来使用javadoc：嵌入的HTML，或使用“文档标记”。其中，“文档标记”（Doc tags）是一些以“@”开头的命令，置于注释行的起始处（但前导的“*”会被忽略）。</p>

  <p>有三种类型的注释文档，它们对应于位于注释后面的元素：类、变量或者方法。也就是说，一个类注释正好位于一个类定义之前；变量注释正好位于变量定义之前；而一个方法定义正好位于一个方法定义的前面。如下面这个简单的例子所示：</p>

  <p>/** 一个类注释 */</p>

  <p>public class docTest {</p>

  <p>/** 一个变量注释 */</p>

  <p>public int i;</p>

  <p>/** 一个方法注释 */</p>

  <p>public void f() {}</p>

  <p>}</p>

  <p>注意javadoc只能为public（公共）和protected（受保护）成员处理注释文档。“private”（私有）和“友好”（详见5章）成员的注释会被忽略，我们看不到任何输出（也可以用-private标记包括private成员）。这样做是有道理的，因为只有public和protected成员才可在文件之外使用，这是客户程序员的希望。然而，所有类注释都会包含到输出结果里。</p>

  <p>上述代码的输出是一个HTML文件，它与其他Java文档具有相同的标准格式。因此，用户会非常熟悉这种格式，可在您设计的类中方便地“漫游”。设计程序时，请务必考虑输入上述代码，用javadoc处理一下，观看最终HTML文件的效果如何。</p>

  <p>2.8.3 嵌入HTML</p>

  <p>javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够充分利用HTML的巨大威力。当然，我们的最终动机是格式化代码，不是为了哗众取宠。下面列出一个例子：</p>

  <p>/**</p>

  <p>* &lt;pre&gt;</p>

  <p>* System.out.println(new Date());</p>

  <p>* &lt;/pre&gt;</p>

  <p>*/</p>

  <p>亦可象在其他Web文档里那样运用HTML，对普通文本进行格式化，使其更具条理、更加美观：</p>

  <p>/**</p>

  <p>* 您&lt;em&gt;甚至&lt;/em&gt;可以插入一个列表：</p>

  <p>* &lt;ol&gt;</p>

  <p>* &lt;li&gt; 项目一</p>

  <p>* &lt;li&gt; 项目二</p>

  <p>* &lt;li&gt; 项目三</p>

  <p>* &lt;/ol&gt;</p>

  <p>*/</p>

  <p>注意在文档注释中，位于一行最开头的星号会被javadoc丢弃。同时丢弃的还有前导空格。javadoc会对所有内容进行格式化，使其与标准的文档外观相符。不要将&lt;h1&gt;或&lt;hr&gt;这样的标题当作嵌入HTML使用，因为javadoc会插入自己的标题，我们给出的标题会与之冲撞。</p>

  <p>所有类型的注释文档――类、变量和方法――都支持嵌入HTML。</p>

  <p>2.8.4 @see：引用其他类</p>

  <p>所有三种类型的注释文档都可包含@see标记，它允许我们引用其他类里的文档。对于这个标记，javadoc会生成相应的HTML，将其直接链接到其他文档。格式如下：</p>

  <p>@see 类名</p>

  <p>@see 完整类名</p>

  <p>@see 完整类名#方法名</p>

  <p>每一格式都会在生成的文档里自动加入一个超链接的“See Also”（参见）条目。注意javadoc不会检查我们指定的超链接，不会验证它们是否有效。</p>

  <p>2.8.5 类文档标记</p>

  <p>随同嵌入HTML和@see引用，类文档还可以包括用于版本信息以及作者姓名的标记。类文档亦可用于“接口”目的（本书后面会详细解释）。</p>

  <p>1. @version</p>

  <p>格式如下：</p>

  <p>@version 版本信息</p>

  <p>其中，“版本信息”代表任何适合作为版本说明的资料。若在javadoc命令行使用了“-version”标记，就会从生成的HTML文档里提取出版本信息。</p>

  <p>2. @author</p>

  <p>格式如下：</p>

  <p>@author 作者信息</p>

  <p>其中，“作者信息”包括您的姓名、电子函件地址或者其他任何适宜的资料。若在javadoc命令行使用了“-author”标记，就会专门从生成的HTML文档里提取出作者信息。</p>

  <p>可为一系列作者使用多个这样的标记，但它们必须连续放置。全部作者信息会一起存入最终HTML代码的单独一个段落里。</p>

  <p>2.8.6 变量文档标记</p>

  <p>变量文档只能包括嵌入的HTML以及@see引用。</p>

  <p>2.8.7 方法文档标记</p>

  <p>除嵌入HTML和@see引用之外，方法还允许使用针对参数、返回值以及违例的文档标记。</p>

  <p>1. @param</p>

  <p>格式如下：</p>

  <p>@param 参数名 说明</p>

  <p>其中，“参数名”是指参数列表内的标识符，而“说明”代表一些可延续到后续行内的说明文字。一旦遇到一个新文档标记，就认为前一个说明结束。可使用任意数量的说明，每个参数一个。</p>

  <p>2. @return</p>

  <p>格式如下：</p>

  <p>@return 说明</p>

  <p>其中，“说明”是指返回值的含义。它可延续到后面的行内。</p>

  <p>3. @exception</p>

  <p>有关“违例”（Exception）的详细情况，我们会在第9章讲述。简言之，它们是一些特殊的对象，若某个方法失败，就可将它们“扔出”对象。调用一个方法时，尽管只有一个违例对象出现，但一些特殊的方法也许能产生任意数量的、不同类型的违例。所有这些违例都需要说明。所以，违例标记的格式如下：</p>

  <p>@exception 完整类名 说明</p>

  <p>其中，“完整类名”明确指定了一个违例类的名字，它是在其他某个地方定义好的。而“说明”（同样可以延续到下面的行）告诉我们为什么这种特殊类型的违例会在方法调用中出现。</p>

  <p>4. @deprecated</p>

  <p>这是Java 1.1的新特性。该标记用于指出一些旧功能已由改进过的新功能取代。该标记的作用是建议用户不必再使用一种特定的功能，因为未来改版时可能摒弃这一功能。若将一个方法标记为@deprecated，则使用该方法时会收到编译器的警告。</p>

  <p>2.8.8 文档示例</p>

  <p>下面还是我们的第一个Java程序，只不过已加入了完整的文档注释：</p>

  <p>92页程序</p>

  <p>第一行：</p>

  <p>//: Property.java</p>

  <p>采用了我自己的方法：将一个“:”作为特殊的记号，指出这是包含了源文件名字的一个注释行。最后一行也用这样的一条注释结尾，它标志着源代码清单的结束。这样一来，可将代码从本书的正文中方便地提取出来，并用一个编译器检查。这方面的细节在第17章讲述。</p>

  <p>2.9 编码样式</p>

  <p>一个非正式的Java编程标准是大写一个类名的首字母。若类名由几个单词构成，那么把它们紧靠到一起（也就是说，不要用下划线来分隔名字）。此外，每个嵌入单词的首字母都采用大写形式。例如：</p>

  <p>class AllTheColorsOfTheRainbow { // ...}</p>

  <p>对于其他几乎所有内容：方法、字段（成员变量）以及对象句柄名称，可接受的样式与类样式差不多，只是标识符的第一个字母采用小写。例如：</p>

  <p>class AllTheColorsOfTheRainbow {</p>

  <p>int anIntegerRepresentingColors;</p>

  <p>void changeTheHueOfTheColor(int newHue) {</p>

  <p>// ...</p>

  <p>}</p>

  <p>// ...</p>

  <p>}</p>

  <p>当然，要注意用户也必须键入所有这些长名字，而且不能输错。</p>

  <p>2.10 总结</p>

  <p>通过本章的学习，大家已接触了足够多的Java编程知识，已知道如何自行编写一个简单的程序。此外，对语言的总体情况以及一些基本思想也有了一定程度的认识。然而，本章所有例子的模式都是单线形式的“这样做，再那样做，然后再做另一些事情”。如果想让程序作出一项选择，又该如何设计呢？例如，“假如这样做的结果是红色，就那样做；如果不是，就做另一些事情”。对于这种基本的编程方法，下一章会详细说明在Java里是如何实现的。</p>

  <p>2.11 练习</p>

  <p>(1) 参照本章的第一个例子，创建一个“Hello，World”程序，在屏幕上简单地显示这句话。注意在自己的类里只需一个方法（“main”方法会在程序启动时执行）。记住要把它设为static形式，并置入自变量列表――即使根本不会用到这个列表。用javac编译这个程序，再用java运行它。</p>

  <p>(2) 写一个程序，打印出从命令行获取的三个自变量。</p>

  <p>(3) 找出Property.java第二个版本的代码，这是一个简单的注释文档示例。请对文件执行javadoc，并在自己的Web浏览器里观看结果。</p>

  <p>(4) 以练习(1)的程序为基础，向其中加入注释文档。利用javadoc，将这个注释文档提取为一个HTML文件，并用Web浏览器观看。</p>

  <p>英文版主页 | 中文版主页 | 详细目录 | 关于译者</p>

  <p>-------------------------------------------------</p>

  <p>TXT书库 www.16txt.com</p>

  <p>-------------------------------------------------第3章 控制程序流程</p>

  <p>“就象任何有感知的生物一样，程序必须能操纵自己的世界，在执行过程中作出判断与选择。”</p>

  <p>在Java里，我们利用运算符操纵对象和数据，并用执行控制语句作出选择。Java是建立在C++基础上的，所以对C和C++程序员来说，对Java这方面的大多数语句和运算符都应是非常熟悉的。当然，Java也进行了自己的一些改进与简化工作。</p>

  <p>3.1 使用Java运算符</p>

  <p>运算符以一个或多个自变量为基础，可生成一个新值。自变量采用与原始方法调用不同的一种形式，但效果是相同的。根据以前写程序的经验，运算符的常规概念应该不难理解。</p>

  <p>加号（+）、减号和负号（-）、乘号（*）、除号（/）以及等号（=）的用法与其他所有编程语言都是类似的。</p>

  <p>所有运算符都能根据自己的运算对象生成一个值。除此以外，一个运算符可改变运算对象的值，这叫作“副作用”（Side Effect）。运算符最常见的用途就是修改自己的运算对象，从而产生副作用。但要注意生成的值亦可由没有副作用的运算符生成。</p>

  <p>几乎所有运算符都只能操作“主类型”（Primitives）。唯一的例外是“=”、“==”和“!=”，它们能操作所有对象（也是对象易令人混淆的一个地方）。除此以外，String类支持“+”和“+=”。</p>

  <p>3.1.1 优先级</p>

  <p>运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java对计算顺序作出了特别的规定。其中，最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则，所以应该用括号明确规定计算顺序。例如：</p>

  <p>A = X + Y - 2/2 + Z;</p>

  <p>为上述表达式加上括号后，就有了一个不同的含义。</p>

  <p>A = X + (Y - 2)/(2 + Z);</p>

  <p>3.1.2 赋值</p>

  <p>赋值是用等号运算符（=）进行的。它的意思是“取得右边的值，把它复制到左边”。右边的值可以是任何常数、变量或者表达式，只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说，它必须有一个物理性的空间来保存右边的值。举个例子来说，可将一个常数赋给一个变量（A=4;），但不可将任何东西赋给一个常数（比如不能4=A）。</p>

  <p>对主数据类型的赋值是非常直接的。由于主类型容纳了实际的值，而且并非指向一个对象的句柄，所以在为其赋值的时候，可将来自一个地方的内容复制到另一个地方。例如，假设为主类型使用“A=B”，那么B处的内容就复制到A。若接着又修改了A，那么B根本不会受这种修改的影响。作为一名程序员，这应成为自己的常识。</p>

  <p>但在为对象“赋值”的时候，情况却发生了变化。对一个对象进行操作时，我们真正操作的是它的句柄。所以倘若“从一个对象到另一个对象”赋值，实际就是将句柄从一个地方复制到另一个地方。这意味着假若为对象使用“C=D”，那么C和D最终都会指向最初只有D才指向的那个对象。下面这个例子将向大家阐示这一点。</p>

  <p>这里有一些题外话。在后面，大家在代码示例里看到的第一个语句将是“package 03”使用的“package”语句，它代表本书第3章。本书每一章的第一个代码清单都会包含象这样的一个“package”（封装、打包、包裹）语句，它的作用是为那一章剩余的代码建立章节编号。在第17章，大家会看到第3章的所有代码清单（除那些有不同封装名称的以外）都会自动置入一个名为c03的子目录里；第4章的代码置入c04；以此类推。所有这些都是通过第17章展示的CodePackage.java程序实现的；“封装”的基本概念会在第5章进行详尽的解释。就目前来说，大家只需记住象“package 03”这样的形式只是用于为某一章的代码清单建立相应的子目录。</p>

  <p>为运行程序，必须保证在classpath里包含了我们安装本书源码文件的根目录（那个目录里包含了c02，c03c，c04等等子目录）。</p>

  <p>对于Java后续的版本（1.1.4和更高版本），如果您的main()用package语句封装到一个文件里，那么必须在程序名前面指定完整的包裹名称，否则不能运行程序。在这种情况下，命令行是：</p>

  <p>java c03.Assignment</p>

  <p>运行位于一个“包裹”里的程序时，随时都要注意这方面的问题。</p>

  <p>下面是例子：//: Assignment.java</p>

  <p>// Assignment with objects is a bit tricky</p>

  <p>package c03;</p>

  <p>class Number {</p>

  <p>int i;</p>

  <p>}</p>

  <p>public class Assignment {</p>

  <p>public static void main(String[] args) {</p>

  <p>Number n1 = new Number();</p>

  <p>Number n2 = new Number();</p>

  <p>n1.i = 9;</p>

  <p>n2.i = 47;</p>

  <p>System.out.println("1: n1.i: " + n1.i +</p>

  <p>", n2.i: " + n2.i);</p>

  <p>n1 = n2;</p>

  <p>System.out.println("2: n1.i: " + n1.i +</p>

  <p>", n2.i: " + n2.i);</p>

  <p>n1.i = 27;</p>

  <p>System.out.println("3: n1.i: " + n1.i +</p>

  <p>", n2.i: " + n2.i);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>Number类非常简单，它的两个实例（n1和n2）是在main()里创建的。每个Number中的i值都赋予了一个不同的值。随后，将n2赋给n1，而且n1发生改变。在许多程序设计语言中，我们都希望n1和n2任何时候都相互独立。但由于我们已赋予了一个句柄，所以下面才是真实的输出：</p>

  <p>1: n1.i: 9, n2.i: 47</p>

  <p>2: n1.i: 47, n2.i: 47</p>

  <p>3: n1.i: 27, n2.i: 27</p>

  <p>看来改变n1的同时也改变了n2！这是由于无论n1还是n2都包含了相同的句柄，它指向相同的对象（最初的句柄位于n1内部，指向容纳了值9的一个对象。在赋值过程中，那个句柄实际已经丢失；它的对象会由“垃圾收集器”自动清除）。</p>

  <p>这种特殊的现象通常也叫作“别名”，是Java操作对象的一种基本方式。但假若不愿意在这种情况下出现别名，又该怎么操作呢？可放弃赋值，并写入下述代码：</p>

  <p>n1.i = n2.i;</p>

  <p>这样便可保留两个独立的对象，而不是将n1和n2绑定到相同的对象。但您很快就会意识到，这样做会使对象内部的字段处理发生混乱，并与标准的面向对象设计准则相悖。由于这并非一个简单的话题，所以留待第12章详细论述，那一章是专门讨论别名的。其时，大家也会注意到对象的赋值会产生一些令人震惊的效果。</p>

  <p>1. 方法调用中的别名处理</p>

  <p>将一个对象传递到方法内部时，也会产生别名现象。</p>

  <p>//: PassObject.java</p>

  <p>// Passing objects to methods can be a bit tricky</p>

  <p>class Letter {</p>

  <p>char c;</p>

  <p>}</p>

  <p>public class PassObject {</p>

  <p>static void f(Letter y) {</p>

  <p>y.c = 'z';</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>Letter x = new Letter();</p>

  <p>x.c = 'a';</p>

  <p>System.out.println("1: x.c: " + x.c);</p>

  <p>f(x);</p>

  <p>System.out.println("2: x.c: " + x.c);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>在许多程序设计语言中，f()方法表面上似乎要在方法的作用域内制作自己的自变量Letter y的一个副本。但同样地，实际传递的是一个句柄。所以下面这个程序行：</p>

  <p>y.c = 'z';</p>

  <p>实际改变的是f()之外的对象。输出结果如下：</p>

  <p>1: x.c: a</p>

  <p>2: x.c: z</p>

  <p>别名和它的对策是非常复杂的一个问题。尽管必须等至第12章才可获得所有答案，但从现在开始就应加以重视，以便提早发现它的缺点。</p>

  <p>3.1.3 算术运算符</p>

  <p>Java的基本算术运算符与其他大多数程序设计语言是相同的。其中包括加号（+）、减号（-）、除号（/）、乘号（*）以及模数（%，从整数除法中获得余数）。整数除法会直接砍掉小数，而不是进位。</p>

  <p>Java也用一种简写形式进行运算，并同时进行赋值操作。这是由等号前的一个运算符标记的，而且对于语言中的所有运算符都是固定的。例如，为了将4加到变量x，并将结果赋给x，可用：x+=4。</p>

  <p>下面这个例子展示了算术运算符的各种用法：</p>

  <p>//: MathOps.java</p>

  <p>// Demonstrates the mathematical operators</p>

  <p>import java.util.*;</p>

  <p>public class MathOps {</p>

  <p>// Create a shorthand to save typing:</p>

  <p>static void prt(String s) {</p>

  <p>System.out.println(s);</p>

  <p>}</p>

  <p>// shorthand to print a string and an int:</p>

  <p>static void pInt(String s, int i) {</p>

  <p>prt(s + " = " + i);</p>

  <p>}</p>

  <p>// shorthand to print a string and a float:</p>

  <p>static void pFlt(String s, float f) {</p>

  <p>prt(s + " = " + f);</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>// Create a random number generator,</p>

  <p>// seeds with current time by default:</p>

  <p>Random rand = new Random();</p>

  <p>int i, j, k;</p>

  <p>// '%' limits maximum value to 99:</p>

  <p>j = rand.nextInt() % 100;</p>

  <p>k = rand.nextInt() % 100;</p>

  <p>pInt("j",j); pInt("k",k);</p>

  <p>i = j + k; pInt("j + k", i);</p>

  <p>i = j - k; pInt("j - k", i);</p>

  <p>i = k / j; pInt("k / j", i);</p>

  <p>i = k * j; pInt("k * j", i);</p>

  <p>i = k % j; pInt("k % j", i);</p>

  <p>j %= k; pInt("j %= k", j);</p>

  <p>// Floating-point number tests:</p>

  <p>float u,v,w; // applies to doubles, too</p>

  <p>v = rand.nextFloat();</p>

  <p>w = rand.nextFloat();</p>

  <p>pFlt("v", v); pFlt("w", w);</p>

  <p>u = v + w; pFlt("v + w", u);</p>

  <p>u = v - w; pFlt("v - w", u);</p>

  <p>u = v * w; pFlt("v * w", u);</p>

  <p>u = v / w; pFlt("v / w", u);</p>

  <p>// the following also works for</p>

  <p>// char, byte, short, int, long,</p>

  <p>// and double:</p>

  <p>u += v; pFlt("u += v", u);</p>

  <p>u -= v; pFlt("u -= v", u);</p>

  <p>u *= v; pFlt("u *= v", u);</p>

  <p>u /= v; pFlt("u /= v", u);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>我们注意到的第一件事情就是用于打印（显示）的一些快捷方法：prt()方法打印一个String；pInt()先打印一个String，再打印一个int；而pFlt()先打印一个String，再打印一个float。当然，它们最终都要用System.out.println()结尾。</p>

  <p>为生成数字，程序首先会创建一个Random（随机）对象。由于自变量是在创建过程中传递的，所以Java将当前时间作为一个“种子值”，由随机数生成器利用。通过Random对象，程序可生成许多不同类型的随机数字。做法很简单，只需调用不同的方法即可：nextInt()，nextLong()，nextFloat()或者nextDouble()。</p>

  <p>若随同随机数生成器的结果使用，模数运算符（%）可将结果限制到运算对象减1的上限（本例是99）之下。</p>

  <p>1. 一元加、减运算符</p>

  <p>一元减号（-）和一元加号（+）与二元加号和减号都是相同的运算符。根据表达式的书写形式，编译器会自动判断使用哪一种。例如下述语句：</p>

  <p>x = -a;</p>

  <p>它的含义是显然的。编译器能正确识别下述语句：</p>

  <p>x = a * -b;</p>

  <p>但读者会被搞糊涂，所以最好更明确地写成：</p>

  <p>x = a * (-b);</p>

  <p>一元减号得到的运算对象的负值。一元加号的含义与一元减号相反，虽然它实际并不做任何事情。</p>

  <p>3.1.4 自动递增和递减</p>

  <p>和C类似，Java提供了丰富的快捷运算方式。这些快捷运算可使代码更清爽，更易录入，也更易读者辨读。</p>

  <p>两种很不错的快捷运算方式是递增和递减运算符（常称作“自动递增”和“自动递减”运算符）。其中，递减运算符是“--”，意为“减少一个单位”；递增运算符是“++”，意为“增加一个单位”。举个例子来说，假设A是一个int（整数）值，则表达式++A就等价于（A = A + 1）。递增和递减运算符结果生成的是变量的值。</p>

  <p>对每种类型的运算符，都有两个版本可供选用；通常将其称为“前缀版”和“后缀版”。“前递增”表示++运算符位于变量或表达式的前面；而“后递增”表示++运算符位于变量或表达式的后面。类似地，“前递减”意味着--运算符位于变量或表达式的前面；而“后递减”意味着--运算符位于变量或表达式的后面。对于前递增和前递减（如++A或--A），会先执行运算，再生成值。而对于后递增和后递减（如A++或A--），会先生成值，再执行运算。下面是一个例子：</p>

  <p>//: AutoInc.java</p>

  <p>// Demonstrates the ++ and -- operators</p>

  <p>public class AutoInc {</p>

  <p>public static void main(String[] args) {</p>

  <p>int i = 1;</p>

  <p>prt("i : " + i);</p>

  <p>prt("++i : " + ++i); // Pre-increment</p>

  <p>prt("i++ : " + i++); // Post-increment</p>

  <p>prt("i : " + i);</p>

  <p>prt("--i : " + --i); // Pre-decrement</p>

  <p>prt("i-- : " + i--); // Post-decrement</p>

  <p>prt("i : " + i);</p>

  <p>}</p>

  <p>static void prt(String s) {</p>

  <p>System.out.println(s);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>该程序的输出如下：</p>

  <p>i : 1</p>

  <p>++i : 2</p>

  <p>i++ : 2</p>

  <p>i : 3</p>

  <p>--i : 2</p>

  <p>i-- : 2</p>

  <p>i : 1</p>

  <p>从中可以看到，对于前缀形式，我们在执行完运算后才得到值。但对于后缀形式，则是在运算执行之前就得到值。它们是唯一具有“副作用”的运算符（除那些涉及赋值的以外）。也就是说，它们会改变运算对象，而不仅仅是使用自己的值。</p>

  <p>递增运算符正是对“C++”这个名字的一种解释，暗示着“超载C的一步”。在早期的一次Java演讲中，Bill Joy（始创人之一）声称“Java=C++--”（C加加减减），意味着Java已去除了C++一些没来由折磨人的地方，形成一种更精简的语言。正如大家会在这本书中学到的那样，Java的许多地方都得到了简化，所以Java的学习比C++更容易。</p>

  <p>3.1.5 关系运算符</p>

  <p>关系运算符生成的是一个“布尔”（Boolean）结果。它们评价的是运算对象值之间的关系。若关系是真实的，关系表达式会生成true（真）；若关系不真实，则生成false（假）。关系运算符包括小于（&lt;）、大于（&gt;）、小于或等于（&lt;=）、大于或等于（&gt;=）、等于（==）以及不等于（!=）。等于和不等于适用于所有内建的数据类型，但其他比较不适用于boolean类型。</p>

  <p>1. 检查对象是否相等</p>

  <p>关系运算符==和!=也适用于所有对象，但它们的含义通常会使初涉Java领域的人找不到北。下面是一个例子：</p>

  <p>//: Equivalence.java</p>

  <p>public class Equivalence {</p>

  <p>public static void main(String[] args) {</p>

  <p>Integer n1 = new Integer(47);</p>

  <p>Integer n2 = new Integer(47);</p>

  <p>System.out.println(n1 == n2);</p>

  <p>System.out.println(n1 != n2);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>其中，表达式System.out.println(n1 == n2)可打印出内部的布尔比较结果。一般人都会认为输出结果肯定先是true，再是false，因为两个Integer对象都是相同的。但尽管对象的内容相同，句柄却是不同的，而==和!=比较的正好就是对象句柄。所以输出结果实际上先是false，再是true。这自然会使第一次接触的人感到惊奇。</p>

  <p>若想对比两个对象的实际内容是否相同，又该如何操作呢？此时，必须使用所有对象都适用的特殊方法equals()。但这个方法不适用于“主类型”，那些类型直接使用==和!=即可。下面举例说明如何使用：</p>

  <p>//: EqualsMethod.java</p>

  <p>public class EqualsMethod {</p>

  <p>public static void main(String[] args) {</p>

  <p>Integer n1 = new Integer(47);</p>

  <p>Integer n2 = new Integer(47);</p>

  <p>System.out.println(n1.equals(n2));</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>正如我们预计的那样，此时得到的结果是true。但事情并未到此结束！假设您创建了自己的类，就象下面这样：</p>

  <p>//: EqualsMethod2.java</p>

  <p>class Value {</p>

  <p>int i;</p>

  <p>}</p>

  <p>public class EqualsMethod2 {</p>

  <p>public static void main(String[] args) {</p>

  <p>Value v1 = new Value();</p>

  <p>Value v2 = new Value();</p>

  <p>v1.i = v2.i = 100;</p>

  <p>System.out.println(v1.equals(v2));</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>此时的结果又变回了false！这是由于equals()的默认行为是比较句柄。所以除非在自己的新类中改变了equals()，否则不可能表现出我们希望的行为。不幸的是，要到第7章才会学习如何改变行为。但要注意equals()的这种行为方式同时或许能够避免一些“灾难”性的事件。</p>

  <p>大多数Java类库都实现了equals()，所以它实际比较的是对象的内容，而非它们的句柄。</p>

  <p>3.1.6 逻辑运算符</p>

  <p>逻辑运算符AND（&amp;&amp;）、OR（||）以及NOT（!）能生成一个布尔值（true或false）――以自变量的逻辑关系为基础。下面这个例子向大家展示了如何使用关系和逻辑运算符。</p>

  <p>//: Bool.java</p>

  <p>// Relational and logical operators</p>

  <p>import java.util.*;</p>

  <p>public class Bool {</p>

  <p>public static void main(String[] args) {</p>

  <p>Random rand = new Random();</p>

  <p>int i = rand.nextInt() % 100;</p>

  <p>int j = rand.nextInt() % 100;</p>

  <p>prt("i = " + i);</p>

  <p>prt("j = " + j);</p>

  <p>prt("i &gt; j is " + (i &gt; j));</p>

  <p>prt("i &lt; j is " + (i &lt; j));</p>

  <p>prt("i &gt;= j is " + (i &gt;= j));</p>

  <p>prt("i &lt;= j is " + (i &lt;= j));</p>

  <p>prt("i == j is " + (i == j));</p>

  <p>prt("i != j is " + (i != j));</p>

  <p>// Treating an int as a boolean is</p>

  <p>// not legal Java</p>

  <p>//! prt("i &amp;&amp; j is " + (i &amp;&amp; j));</p>

  <p>//! prt("i || j is " + (i || j));</p>

  <p>//! prt("!i is " + !i);</p>

  <p>prt("(i &lt; 10) &amp;&amp; (j &lt; 10) is "</p>

  <p>+ ((i &lt; 10) &amp;&amp; (j &lt; 10)) );</p>

  <p>prt("(i &lt; 10) || (j &lt; 10) is "</p>

  <p>+ ((i &lt; 10) || (j &lt; 10)) );</p>

  <p>}</p>

  <p>static void prt(String s) {</p>

  <p>System.out.println(s);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>只可将AND，OR或NOT应用于布尔值。与在C及C++中不同，不可将一个非布尔值当作布尔值在逻辑表达式中使用。若这样做，就会发现尝试失败，并用一个“//!”标出。然而，后续的表达式利用关系比较生成布尔值，然后对结果进行逻辑运算。</p>

  <p>输出列表看起来象下面这个样子：</p>

  <p>i = 85</p>

  <p>j = 4</p>

  <p>i &gt; j is true</p>

  <p>i &lt; j is false</p>

  <p>i &gt;= j is true</p>

  <p>i &lt;= j is false</p>

  <p>i == j is false</p>

  <p>i != j is true</p>

  <p>(i &lt; 10) &amp;&amp; (j &lt; 10) is false</p>

  <p>(i &lt; 10) || (j &lt; 10) is true</p>

  <p>注意若在预计为String值的地方使用，布尔值会自动转换成适当的文本形式。</p>

  <p>在上述程序中，可将对int的定义替换成除boolean以外的其他任何主数据类型。但要注意，对浮点数字的比较是非常严格的。即使一个数字仅在小数部分与另一个数字存在极微小的差异，仍然认为它们是“不相等”的。即使一个数字只比零大一点点（例如2不停地开平方根），它仍然属于“非零”值。</p>

  <p>1. 短路</p>

  <p>操作逻辑运算符时，我们会遇到一种名为“短路”的情况。这意味着只有明确得出整个表达式真或假的结论，才会对表达式进行逻辑求值。因此，一个逻辑表达式的所有部分都有可能不进行求值：</p>

  <p>//: ShortCircuit.java</p>

  <p>// Demonstrates short-circuiting behavior</p>

  <p>// with logical operators.</p>

  <p>public class ShortCircuit {</p>

  <p>static boolean test1(int val) {</p>

  <p>System.out.println("test1(" + val + ")");</p>

  <p>System.out.println("result: " + (val &lt; 1));</p>

  <p>return val &lt; 1;</p>

  <p>}</p>

  <p>static boolean test2(int val) {</p>

  <p>System.out.println("test2(" + val + ")");</p>

  <p>System.out.println("result: " + (val &lt; 2));</p>

  <p>return val &lt; 2;</p>

  <p>}</p>

  <p>static boolean test3(int val) {</p>

  <p>System.out.println("test3(" + val + ")");</p>

  <p>System.out.println("result: " + (val &lt; 3));</p>

  <p>return val &lt; 3;</p>

  <p>}</p>

  <p>public static void main(String[] args) {</p>

  <p>if(test1(0) &amp;&amp; test2(2) &amp;&amp; test3(2))</p>

  <p>System.out.println("expression is true");</p>

  <p>else</p>

  <p>System.out.println("expression is false");</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>每次测试都会比较自变量，并返回真或假。它不会显示与准备调用什么有关的资料。测试在下面这个表达式中进行：</p>

  <p>if(test1(0)) &amp;&amp; test2(2) &amp;&amp; test3(2))</p>

  <p>很自然地，你也许认为所有这三个测试都会得以执行。但希望输出结果不至于使你大吃一惊：</p>

  <p>if(test1(0) &amp;&amp; test2(2) &amp;&amp; test3(2))</p>

  <p>第一个测试生成一个true结果，所以表达式求值会继续下去。然而，第二个测试产生了一个false结果。由于这意味着整个表达式肯定为false，所以为什么还要继续剩余的表达式呢？这样做只会徒劳无益。事实上，“短路”一词的由来正种因于此。如果一个逻辑表达式的所有部分都不必执行下去，那么潜在的性能提升将是相当可观的。</p>

  <p>3.1.7 按位运算符</p>

  <p>按位运算符允许我们操作一个整数主数据类型中的单个“比特”，即二进制位。按位运算符会对两个自变量中对应的位执行布尔代数，并最终生成一个结果。</p>

  <p>按位运算来源于C语言的低级操作。我们经常都要直接操纵硬件，需要频繁设置硬件寄存器内的二进制位。Java的设计初衷是嵌入电视顶置盒内，所以这种低级操作仍被保留下来了。然而，由于操作系统的进步，现在也许不必过于频繁地进行按位运算。</p>

  <p>若两个输入位都是1，则按位AND运算符（&amp;）在输出位里生成一个1；否则生成0。若两个输入位里至少有一个是1，则按位OR运算符（|）在输出位里生成一个1；只有在两个输入位都是0的情况下，它才会生成一个0。若两个输入位的某一个是1，但不全都是1，那么按位XOR（^，异或）在输出位里生成一个1。按位NOT（~，也叫作“非”运算符）属于一元运算符；它只对一个自变量进行操作（其他所有运算符都是二元运算符）。按位NOT生成与输入位的相反的值――若输入0，则输出1；输入1，则输出0。</p>

  <p>按位运算符和逻辑运算符都使用了同样的字符，只是数量不同。因此，我们能方便地记忆各自的含义：由于“位”是非常“小”的，所以按位运算符仅使用了一个字符。</p>

  <p>按位运算符可与等号（=）联合使用，以便合并运算及赋值：&amp;=，|=和^=都是合法的（由于~是一元运算符，所以不可与=联合使用）。</p>

  <p>我们将boolean（布尔）类型当作一种“单位”或“单比特”值对待，所以它多少有些独特的地方。我们可执行按位AND，OR和XOR，但不能执行按位NOT（大概是为了避免与逻辑NOT混淆）。对于布尔值，按位运算符具有与逻辑运算符相同的效果，只是它们不会中途“短路”。此外，针对布尔值进行的按位运算为我们新增了一个XOR逻辑运算符，它并未包括在“逻辑”运算符的列表中。在移位表达式中，我们被禁止使用布尔运算，原因将在下面解释。</p>

  <p>3.1.8 移位运算符</p>

  <p>移位运算符面向的运算对象也是二进制的“位”。可单独用它们处理整数类型（主类型的一种）。左移位运算符（&lt;&lt;）能将运算符左边的运算对象向左移动运算符右侧指定的位数（在低位补0）。“有符号”右移位运算符（&gt;&gt;）则将运算符左边的运算对象向右移动运算符右侧指定的位数。“有符号”右移位运算符使用了“符号扩展”：若值为正，则在高位插入0；若值为负，则在高位插入1。Java也添加了一种“无符号”右移位运算符（&gt;&gt;&gt;），它使用了“零扩展”：无论正负，都在高位插入0。这一运算符是C或C++没有的。</p>

  <p>若对char，byte或者short进行移位处理，那么在移位进行之前，它们会自动转换成一个int。只有右侧的5个低位才会用到。这样可防止我们在一个int数里移动不切实际的位数。若对一个long值进行处理，最后得到的结果也是long。此时只会用到右侧的6个低位，防止移动超过long值里现成的位数。但在进行“无符号”右移位时，也可能遇到一个问题。若对byte或short值进行右移位运算，得到的可能不是正确的结果（Java 1.0和Java 1.1特别突出）。它们会自动转换成int类型，并进行右移位。但“零扩展”不会发生，所以在那些情况下会得到-1的结果。可用下面这个例子检测自己的实现方案：</p>

  <p>//: URShift.java</p>

  <p>// Test of unsigned right shift</p>

  <p>public class URShift {</p>

  <p>public static void main(String[] args) {</p>

  <p>int i = -1;</p>

  <p>i &gt;&gt;&gt;= 10;</p>

  <p>System.out.println(i);</p>

  <p>long l = -1;</p>

  <p>l &gt;&gt;&gt;= 10;</p>

  <p>System.out.println(l);</p>

  <p>short s = -1;</p>

  <p>s &gt;&gt;&gt;= 10;</p>

  <p>System.out.println(s);</p>

  <p>byte b = -1;</p>

  <p>b &gt;&gt;&gt;= 10;</p>

  <p>System.out.println(b);</p>

  <p>}</p>

  <p>} ///:~</p>

  <p>移位可与等号（&lt;&lt;=或&gt;&gt;=或&gt;&gt;&gt;=）组合使用。此时，运算符左边的值会移动由右边的值指定的位数，再将得到的结果赋回左边的值。</p>

  <p>下面这个例子向大家阐示了如何应用涉及“按位”操作的所有运算符，以及它们的效果：</p>

  <p>//: BitManipulation.java</p>

  <p>// Using the bitwise operators</p>

  <p>import java.util.*;</p>

  <p>public class BitManipulation {</p>

  <p>public static void main(String[] args) {</p>

  <p>Random rand = new Random();</p>

  <p>int i = rand.nextInt();</p>

  <p>int j = rand.nextInt();</p>

  <p>pBinInt("-1", -1);</p>

  <p>pBinInt("+1", +1);</p>

  <p>int maxpos = 2147483647;</p>

  <p>pBinInt("maxpos", maxpos);</p>

  <p>int maxneg = -2147483648;</p>

  <p>pBinInt("maxneg", maxneg);</p>

  <p>pBinInt("i", i);</p>

  <p>pBinInt("~i", ~i);</p>

  <p>pBinInt("-i", -i);</p>

  <p>pBinInt("j", j);</p>

  <p>pBinInt("i &amp; j", i &amp; j);</p>

  <p>pBinInt("i | j", i | j);</p>

  <p>pBinInt("i ^ j", i ^ j);</p>

  <p>pBinInt("i &lt;&lt; 5", i &lt;&lt; 5);</p>

  <p>pBinInt("i &gt;&gt; 5", i &gt;&gt; 5);</p>

  <p>pBinInt("(~i) &gt;&gt; 5", (~i) &gt;&gt; 5);</p>

  <p>pBinInt("i &gt;&gt;&gt; 5", i &gt;&gt;&gt; 5);</p>

  <p>pBinInt("(~i) &gt;&gt;&gt; 5", (~i) &gt;&gt;&gt; 5);</p>

  <p>long l = rand.nextLong();</p>

  <p>long m = rand.nextLong();</p>

  <p>pBinLong("-1L", -1L);</p>

  <p>pBinLong("+1L", +1L);</p>

  <p>long ll = 9223372036854775807L;</p>

  <p>pBinLong("maxpos", ll);</p>

  <p>long lln = -9223372036854775808L;</p>

  <p>pBinLong("maxneg", lln);</p>

  <p>pBinLong("l", l);</p>

  <p>pBinLong("~l", ~l);</p>

  <p>pBinLong("-l", -l);</p>

  <p>pBinLong("m", m);</p>

  <p>pBinLong("l &amp; m", l &amp; m);</p>

  <p>pBinLong("l | m", l | m);</p>

  <p>pBinLong("l ^ m", l ^ m);</p>

  <p>pBinLong("l &lt;&lt; 5", l &lt;&lt; 5);</p>

  <p>pBinLong("l &gt;&gt; 5", l &gt;&gt; 5);</p>

  <p>pBinLong("(~l) &gt;&gt; 5", (~l) &gt;&gt; 5);</p>

  <p>pBinLong("l &gt;&gt;&gt; 5", l &gt;&gt;&gt; 5);</p>

  <p>pBinLong("(~l) &gt;&gt;&gt; 5", (~l) &gt;&gt;&gt; 5);</p>

  <p>}</p>

  <p>static void pBinInt(String s, int i) {</p>

  <p>System.out.println(</p>

  <p>s + ", int: " + i + ", binary: ");</p>

  <p>System.out.print(" ");</p>

  <p>for(int j = 31; j &gt;=0; j--)</p>

  <p>if(((1 &lt;&lt; j) &amp; i) != 0)</p>

  <p>System.out.print("1");</p>

  <p>else</p>

  <p>Sy</p>

  <div class="mbppagebreak"></div>
</body>
</html>
